/**
 * @authors Luke Mahe
 * @authors Eric Bidelman
 * @fileoverview TODO
 */
document.cancelFullScreen = document.webkitCancelFullScreen
		|| document.mozCancelFullScreen;

/**
 * @constructor
 */
function SlideDeck(el) {
	this.curSlide_ = 0;
	this.prevSlide_ = 0;
	this.config_ = null;
	this.container = el || document.querySelector('slides');
	this.slides = [];
	this.controller = null;

	this.getCurrentSlideFromHash_();

	// Call this explicitly. Modernizr.load won't be done until after DOM load.
	this.onDomLoaded_.bind(this)();
}

/**
 * @const
 * @private
 */
SlideDeck.prototype.SLIDE_CLASSES_ = [ 'far-past', 'past', 'current', 'next',
		'far-next' ];

/**
 * @const
 * @private
 */
SlideDeck.prototype.CSS_DIR_ = 'theme/css/';

/**
 * @private
 */
SlideDeck.prototype.getCurrentSlideFromHash_ = function() {
	var slideNo = parseInt(document.location.hash.substr(1));

	if (slideNo) {
		this.curSlide_ = slideNo - 1;
	} else {
		this.curSlide_ = 0;
	}
};

/**
 * @param {number}
 *            slideNo
 */
SlideDeck.prototype.loadSlide = function(slideNo) {
	if (slideNo) {
		this.curSlide_ = slideNo - 1;
		this.updateSlides_();
	}
};

/**
 * @private
 */
SlideDeck.prototype.onDomLoaded_ = function(e) {
	document.body.classList.add('loaded'); // Add loaded class for templates to
	// use.

	this.slides = this.container
			.querySelectorAll('slide:not([hidden]):not(.hidden):not(.backdrop)');

	// If we're on a smartphone, apply special sauce.
	if (Modernizr.mq('only screen and (max-device-width: 480px)')) {
		// var style = document.createElement('link');
		// style.rel = 'stylesheet';
		// style.type = 'text/css';
		// style.href = this.CSS_DIR_ + 'phone.css';
		// document.querySelector('head').appendChild(style);

		// No need for widescreen layout on a phone.
		this.container.classList.remove('layout-widescreen');
	}

	this.loadConfig_(SLIDE_CONFIG);
	this.addEventListeners_();
	this.updateSlides_();

	// Add slide numbers and total slide count metadata to each slide.
	var that = this;
	for (var i = 0, slide; slide = this.slides[i]; ++i) {
		slide.dataset.slideNum = i + 1;
		slide.dataset.totalSlides = this.slides.length;

		slide.addEventListener('click', function(e) {
			if (document.body.classList.contains('overview')) {
				that.loadSlide(this.dataset.slideNum);
				e.preventDefault();
				window.setTimeout(function() {
					that.toggleOverview();
				}, 500);
			}
		}, false);
	}

	// Note: this needs to come after addEventListeners_(), which adds a
	// 'keydown' listener that this controller relies on.

	// Modernizr.touch isn't a sufficient check for devices that support both
	// touch and mouse. Create the controller in all cases.
	// // Also, no need to set this up if we're on mobile.
	// if (!Modernizr.touch) {
	this.controller = new SlideController(this);
	// if (this.controller.isPresenter) {
	// document.body.classList.add('popup');
	// }
	// }
};

/**
 * @private
 */
SlideDeck.prototype.addEventListeners_ = function() {
	document.addEventListener('keydown', this.onBodyKeyDown_.bind(this), false);
	window.addEventListener('popstate', this.onPopState_.bind(this), false);

	// var transEndEventNames = {
	// 'WebkitTransition': 'webkitTransitionEnd',
	// 'MozTransition': 'transitionend',
	// 'OTransition': 'oTransitionEnd',
	// 'msTransition': 'MSTransitionEnd',
	// 'transition': 'transitionend'
	// };
	// 
	// // Find the correct transitionEnd vendor prefix.
	// window.transEndEventName = transEndEventNames[
	// Modernizr.prefixed('transition')];
	// 
	// // When slides are done transitioning, kickoff loading iframes.
	// // Note: we're only looking at a single transition (on the slide). This
	// // doesn't include autobuilds the slides may have. Also, if the slide
	// // transitions on multiple properties (e.g. not just 'all'), this doesn't
	// // handle that case.
	// this.container.addEventListener(transEndEventName, function(e) {
	// this.enableSlideFrames_(this.curSlide_);
	// }.bind(this), false);

	// document.addEventListener('slideenter', function(e) {
	// var slide = e.target;
	// window.setTimeout(function() {
	// this.enableSlideFrames_(e.slideNumber);
	// this.enableSlideFrames_(e.slideNumber + 1);
	// }.bind(this), 300);
	// }.bind(this), false);
};

/**
 * @private
 * @param {Event}
 *            e The pop event.
 */
SlideDeck.prototype.onPopState_ = function(e) {
	if (e.state != null) {
		this.curSlide_ = e.state;
		this.updateSlides_(true);
	}
};

/**
 * @param {Event}
 *            e
 */
SlideDeck.prototype.onBodyKeyDown_ = function(e) {
	if (/^(input|textarea)$/i.test(e.target.nodeName)
			|| e.target.isContentEditable) {
		return;
	}

	// Forward keydowns to the main slides if we're the popup.
	if (this.controller && this.controller.isController) {
		this.controller.sendMsg({
			keyCode : e.keyCode
		});
	}

	switch (e.keyCode) {
		case 13: // Enter
			if (document.body.classList.contains('overview')) {
				this.toggleOverview();
			}
			break;

		case 39: // right arrow
		case 32: // space
		case 34: // PgDn
			this.nextSlide();
			e.preventDefault();
			break;

		case 37: // left arrow
		case 8: // Backspace
		case 33: // PgUp
			this.prevSlide();
			e.preventDefault();
			break;

		case 40: // down arrow
			this.nextSlide();
			e.preventDefault();
			break;

		case 38: // up arrow
			this.prevSlide();
			e.preventDefault();
			break;

		case 72: // H: Toggle code highlighting
			document.body.classList.toggle('highlight-code');
			break;

		case 79: // O: Toggle overview
			this.toggleOverview();
			break;

		case 80: // P
			// if (this.controller && this.controller.isPresenter) {
			document.body.classList.toggle('with-notes');
			// } else if (this.controller && !this.controller.popup) {
			// document.body.classList.toggle('with-notes');
			// }
			break;

		case 82: // R
			// TODO: implement refresh on main slides when popup is refreshed.
			break;

		case 27: // ESC: Hide notes and highlighting
			document.body.classList.remove('with-notes');
			document.body.classList.remove('highlight-code');

			if (document.body.classList.contains('overview')) {
				this.toggleOverview();
			}
			break;

		case 70: // F: Toggle fullscreen
			// Only respect 'f' on body. Don't want to capture keys from an
			// <input>.
			// Also, ignore browser's fullscreen shortcut (cmd+shift+f) so we
			// don't
			// get trapped in fullscreen!
			if (e.target == document.body && !(e.shiftKey && e.metaKey)) {
				if (document.mozFullScreen !== undefined
						&& !document.mozFullScreen) {
					document.body
							.mozRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
				} else if (document.webkitIsFullScreen !== undefined
						&& !document.webkitIsFullScreen) {
					document.body
							.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
				} else {
					document.cancelFullScreen();
				}
			}
			break;

		case 87: // W: Toggle widescreen
			// Only respect 'w' on body. Don't want to capture keys from an
			// <input>.
			if (e.target == document.body && !(e.shiftKey && e.metaKey)) {
				this.container.classList.toggle('layout-widescreen');
			}
			break;
	}
};

/**
 * 
 */
SlideDeck.prototype.focusOverview_ = function() {
	var overview = document.body.classList.contains('overview');

	for (var i = 0, slide; slide = this.slides[i]; i++) {
		slide.style[Modernizr.prefixed('transform')] = overview ? 'translateZ(-2500px) translate('
				+ ((i - this.curSlide_) * 105) + '%, 0%)'
				: '';
	}
};

/**
 */
SlideDeck.prototype.toggleOverview = function() {
	document.body.classList.toggle('overview');

	this.focusOverview_();
};

/**
 * @private
 */
SlideDeck.prototype.loadConfig_ = function(config) {
	if (!config) {
		return;
	}

	this.config_ = config;

	var settings = this.config_.settings;

	this.loadTheme_(settings.theme || []);

	if (settings.favIcon) {
		this.addFavIcon_(settings.favIcon);
	}

	// Prettyprint. Default to on.
	if (!!!('usePrettify' in settings) || settings.usePrettify) {
		require([ 'prettify' ], function() {
			prettyPrint();
		});
	}

	if (settings.analytics) {
		this.loadAnalytics_();
	}

	if (settings.fonts) {
		this.addFonts_(settings.fonts);
	}

	// Builds. Default to on.
	if (!!!('useBuilds' in settings) || settings.useBuilds) {
		this.makeBuildLists_();
	}

	if (settings.title) {
		document.title = settings.title.replace(/<br\/?>/, ' ');
		if (settings.eventInfo && settings.eventInfo.title) {
			document.title += ' - ' + settings.eventInfo.title;
		}
		document.querySelector('[data-config-title]').innerHTML = settings.title;
	}

	if (settings.subtitle) {
		document.querySelector('[data-config-subtitle]').innerHTML = settings.subtitle;
	}

	if (this.config_.presenters) {
		var presenters = this.config_.presenters;
		var dataConfigContact = document.querySelector('[data-config-contact]');

		var html = [];
		if (presenters.length == 1) {
			var p = presenters[0];

			var presenterTitle = [ p.name ];
			if (p.company) {
				presenterTitle.push(p.company);
			}
			html = presenterTitle.join(' - ') + '<br>';

			var gplus = p.gplus ? '<span>g+</span><a href="' + p.gplus + '">'
					+ p.gplus.replace(/https?:\/\//, '') + '</a>' : '';

			var twitter = p.twitter ? '<span>twitter</span>'
					+ '<a href="http://twitter.com/' + p.twitter + '">'
					+ p.twitter + '</a>' : '';

			var www = p.www ? '<span>www</span><a href="' + p.www + '">'
					+ p.www.replace(/https?:\/\//, '') + '</a>' : '';

			var github = p.github ? '<span>github</span><a href="' + p.github
					+ '">' + p.github.replace(/https?:\/\//, '') + '</a>' : '';

			var html2 = [ gplus, twitter, www, github ].join('<br>');

			if (dataConfigContact) {
				dataConfigContact.innerHTML = html2;
			}
		} else {
			for (var i = 0, p; p = presenters[i]; ++i) {
				html.push(p.name + ' - ' + p.company);
			}
			html = html.join('<br>');
			if (dataConfigContact) {
				dataConfigContact.innerHTML = html;
			}
		}

		var dataConfigPresenter = document
				.querySelector('[data-config-presenter]');
		if (dataConfigPresenter) {
			dataConfigPresenter.innerHTML = html;
			if (settings.eventInfo) {
				var date = settings.eventInfo.date;
				var dateInfo = date ? ' - <time>' + date + '</time>' : '';
				dataConfigPresenter.innerHTML += settings.eventInfo.title
						+ dateInfo;
			}
		}
	}

	/* Left/Right tap areas. Default to including. */
	if (!!!('enableSlideAreas' in settings) || settings.enableSlideAreas) {
		var el = document.createElement('div');
		el.classList.add('slide-area');
		el.id = 'prev-slide-area';
		el.addEventListener('click', this.prevSlide.bind(this), false);
		this.container.appendChild(el);

		var el = document.createElement('div');
		el.classList.add('slide-area');
		el.id = 'next-slide-area';
		el.addEventListener('click', this.nextSlide.bind(this), false);
		this.container.appendChild(el);
	}

	if (/*
		 * Modernizr.touch &&
		 */(!!!('enableTouch' in settings) || settings.enableTouch)) {
		var self = this;

		// Note: this prevents mobile zoom in/out but prevents iOS from doing
		// it's crazy scroll over effect and disaligning the slides.
		window.addEventListener('touchstart', function(e) {
			e.preventDefault();
		}, false);

		require([ 'hammer' ], function(Hammer) {

			var pressKey = function(keyCode) {
				var evt = document.createEvent('Event');
				evt.initEvent('keydown', true, true);
				evt.keyCode = keyCode;
				document.body.dispatchEvent(evt);
			};

			var hammer = new Hammer(self.container);

			// previous slide
			hammer.on('swiperight', function() {
				pressKey(37);
			});

			// next slide
			hammer.on('swipeleft', function() {
				pressKey(39);
			});

			// fullscreen
			hammer.on('doubletap', function() {
				pressKey(70);
			});

		});

	}
};

/**
 * @private
 * @param {Array.
 *            <string>} fonts
 */
SlideDeck.prototype.addFonts_ = function(fonts) {
	var el = document.createElement('link');
	el.rel = 'stylesheet';
	el.href = ('https:' == document.location.protocol ? 'https' : 'http')
			+ '://fonts.googleapis.com/css?family=' + fonts.join('|') + '&v2';
	document.querySelector('head').appendChild(el);
};

/**
 * @private
 */
SlideDeck.prototype.buildNextItem_ = function() {
	var slide = this.slides[this.curSlide_];
	var toBuild = slide.querySelector('.to-build');
	var built = slide.querySelector('.build-current');

	if (built) {
		built.classList.remove('build-current');
		if (built.classList.contains('fade')) {
			built.classList.add('build-fade');
		}
	}

	if (!toBuild) {
		var items = slide.querySelectorAll('.build-fade');
		for (var j = 0, item; item = items[j]; j++) {
			item.classList.remove('build-fade');
		}
		return false;
	}

	toBuild.classList.remove('to-build');
	toBuild.classList.add('build-current');

	return true;
};

/**
 * @param {boolean=}
 *            opt_dontPush
 */
SlideDeck.prototype.prevSlide = function(opt_dontPush) {
	if (this.curSlide_ > 0) {
		var bodyClassList = document.body.classList;
		bodyClassList.remove('highlight-code');

		// Toggle off speaker notes if they're showing when we move backwards on
		// the
		// main slides. If we're the speaker notes popup, leave them up.
		// if (this.controller && !this.controller.isPresenter) {
		// bodyClassList.remove('with-notes');
		// } else if (!this.controller) {
		// bodyClassList.remove('with-notes');
		// }

		this.prevSlide_ = this.curSlide_--;

		this.updateSlides_(opt_dontPush);
	}
};

/**
 * @param {boolean=}
 *            opt_dontPush
 */
SlideDeck.prototype.nextSlide = function(opt_dontPush) {
	if (!document.body.classList.contains('overview') && this.buildNextItem_()) {
		return;
	}

	if (this.curSlide_ < this.slides.length - 1) {
		var bodyClassList = document.body.classList;
		bodyClassList.remove('highlight-code');

		// Toggle off speaker notes if they're showing when we advanced on the
		// main
		// slides. If we're the speaker notes popup, leave them up.
		// if (this.controller && !this.controller.isPresenter) {
		// bodyClassList.remove('with-notes');
		// } else if (!this.controller) {
		// bodyClassList.remove('with-notes');
		// }

		this.prevSlide_ = this.curSlide_++;

		this.updateSlides_(opt_dontPush);
	}
};

/* Slide events */

/**
 * Triggered when a slide enter/leave event should be dispatched.
 * 
 * @param {string}
 *            type The type of event to trigger (e.g. 'slideenter',
 *            'slideleave').
 * @param {number}
 *            slideNo The index of the slide that is being left.
 */
SlideDeck.prototype.triggerSlideEvent = function(type, slideNo) {
	var el = this.getSlideEl_(slideNo);
	if (!el) {
		return;
	}

	// Call onslideenter/onslideleave if the attribute is defined on this slide.
	var func = el.getAttribute(type);
	if (func) {
		new Function(func).call(el); // TODO: Don't use new Function() :(
	}

	// Dispatch event to listeners setup using addEventListener.
	var evt = document.createEvent('Event');
	evt.initEvent(type, true, true);
	evt.slideNumber = slideNo + 1; // Make it readable
	evt.slide = el;

	el.dispatchEvent(evt);
};

/**
 * @private
 */
SlideDeck.prototype.updateSlides_ = function(opt_dontPush) {
	var dontPush = opt_dontPush || false;

	var curSlide = this.curSlide_;
	for (var i = 0; i < this.slides.length; ++i) {
		switch (i) {
			case curSlide - 2:
				this.updateSlideClass_(i, 'far-past');
				break;
			case curSlide - 1:
				this.updateSlideClass_(i, 'past');
				break;
			case curSlide:
				this.updateSlideClass_(i, 'current');
				break;
			case curSlide + 1:
				this.updateSlideClass_(i, 'next');
				break;
			case curSlide + 2:
				this.updateSlideClass_(i, 'far-next');
				break;
			default:
				this.updateSlideClass_(i);
				break;
		}
	}
	;

	this.triggerSlideEvent('slideleave', this.prevSlide_);
	this.triggerSlideEvent('slideenter', curSlide);

	// window.setTimeout(this.disableSlideFrames_.bind(this, curSlide - 2),
	// 301);
	// 
	// this.enableSlideFrames_(curSlide - 1); // Previous slide.
	// this.enableSlideFrames_(curSlide + 1); // Current slide.
	// this.enableSlideFrames_(curSlide + 2); // Next slide.

	// Enable current slide's iframes (needed for page loat at current slide).
	this.enableSlideFrames_(curSlide + 1);

	// No way to tell when all slide transitions + auto builds are done.
	// Give ourselves a good buffer to preload the next slide's iframes.
	window.setTimeout(this.enableSlideFrames_.bind(this, curSlide + 2), 1000);

	this.updateHash_(dontPush);

	if (document.body.classList.contains('overview')) {
		this.focusOverview_();
		return;
	}

};

/**
 * @private
 * @param {number}
 *            slideNo
 */
SlideDeck.prototype.enableSlideFrames_ = function(slideNo) {
	var el = this.slides[slideNo - 1];
	if (!el) {
		return;
	}

	var frames = el.querySelectorAll('iframe');
	for (var i = 0, frame; frame = frames[i]; i++) {
		this.enableFrame_(frame);
	}
};

/**
 * @private
 * @param {number}
 *            slideNo
 */
SlideDeck.prototype.enableFrame_ = function(frame) {
	var src = frame.dataset.src;
	if (src && frame.src != src) {
		frame.src = src;
	}
};

/**
 * @private
 * @param {number}
 *            slideNo
 */
SlideDeck.prototype.disableSlideFrames_ = function(slideNo) {
	var el = this.slides[slideNo - 1];
	if (!el) {
		return;
	}

	var frames = el.querySelectorAll('iframe');
	for (var i = 0, frame; frame = frames[i]; i++) {
		this.disableFrame_(frame);
	}
};

/**
 * @private
 * @param {Node}
 *            frame
 */
SlideDeck.prototype.disableFrame_ = function(frame) {
	frame.src = 'about:blank';
};

/**
 * @private
 * @param {number}
 *            slideNo
 */
SlideDeck.prototype.getSlideEl_ = function(no) {
	if ((no < 0) || (no >= this.slides.length)) {
		return null;
	} else {
		return this.slides[no];
	}
};

/**
 * @private
 * @param {number}
 *            slideNo
 * @param {string}
 *            className
 */
SlideDeck.prototype.updateSlideClass_ = function(slideNo, className) {
	var el = this.getSlideEl_(slideNo);

	if (!el) {
		return;
	}

	if (className) {
		el.classList.add(className);
	}

	for (var i = 0, slideClass; slideClass = this.SLIDE_CLASSES_[i]; ++i) {
		if (className != slideClass) {
			el.classList.remove(slideClass);
		}
	}
};

/**
 * @private
 */
SlideDeck.prototype.makeBuildLists_ = function() {
	for (var i = this.curSlide_, slide; slide = this.slides[i]; ++i) {
		var items = slide.querySelectorAll('.build > *');
		for (var j = 0, item; item = items[j]; ++j) {
			if (item.classList) {
				item.classList.add('to-build');
				if (item.parentNode.classList.contains('fade')) {
					item.classList.add('fade');
				}
			}
		}
	}
};

/**
 * @private
 * @param {boolean}
 *            dontPush
 */
SlideDeck.prototype.updateHash_ = function(dontPush) {
	if (!dontPush) {
		var slideNo = this.curSlide_ + 1;
		var hash = '#' + slideNo;
		if (window.history.pushState) {
			window.history.pushState(this.curSlide_, 'Slide ' + slideNo, hash);
		} else {
			window.location.replace(hash);
		}

		// Record GA hit on this slide.
		window['_gaq']
				&& window['_gaq'].push([ '_trackPageview',
						document.location.href ]);
	}
};

/**
 * @private
 * @param {string}
 *            favIcon
 */
SlideDeck.prototype.addFavIcon_ = function(favIcon) {
	var el = document.createElement('link');
	el.rel = 'icon';
	el.type = 'image/png';
	el.href = favIcon;
	document.querySelector('head').appendChild(el);
};

/**
 * @private
 * @param {string}
 *            theme
 */
SlideDeck.prototype.loadTheme_ = function(theme) {
	var styles = [];
	if (theme.constructor.name === 'String') {
		styles.push(theme);
	} else {
		styles = theme;
	}

	for (var i = 0, style; themeUrl = styles[i]; i++) {
		var style = document.createElement('link');
		style.rel = 'stylesheet';
		style.type = 'text/css';
		if (themeUrl.indexOf('http') == -1) {
			style.href = this.CSS_DIR_ + themeUrl + '.css';
		} else {
			style.href = themeUrl;
		}
		document.querySelector('head').appendChild(style);
	}
};

/**
 * @private
 */
SlideDeck.prototype.loadAnalytics_ = function() {
	window._gaq = window['_gaq'] || [];
	window._gaq.push([ '_setAccount', this.config_.settings.analytics ]);
	window._gaq.push([ '_trackPageview' ]);

	requirejs([ 'analytics' ]);
};

// Polyfill missing APIs (if we need to), then create the slide deck.
// iOS < 5 needs classList, dataset, and window.matchMedia. Modernizr contains
// the last one.
(function() {
	Modernizr
			.load({
				test : !!document.body.classList && !!document.body.dataset,
				nope : [ 'js/polyfills/classList.min.js',
						'js/polyfills/dataset.min.js' ],
				complete : function() {
					window.slidedeck = new SlideDeck();
				}
			});
})();
